#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import pwd
from ..._util import Util, CopiedAndSymlinkedDir
from ..._errors import DataDirError
from ..._game import GameBase, GameSpec, MatchContext, GameSupportIconDir
from ..._game_straits import GameStraitForWindows, GameStraitNeedConfigDir


class NeedForSpeed10(GameBase, GameStraitForWindows, GameSupportIconDir):

    class LevelOfDetail:

        @classmethod
        def max(cls):
            ret = cls()
            ret.CarEnvironmentMapEnable = 3
            ret.CarEnvironmentMapUpdateData = 1
            ret.CarLodLevel = 1
            ret.FSAALevel = 3
            ret.MotionBlurEnable = True
            ret.OverBrightEnable = True
            ret.ParticleSystemEnable = True
            ret.PerformanceLevel = 5
            ret.RainEnable = True
            ret.RoadReflectionEnable = True
            ret.ShadowDetail = 2
            ret.TextureFiltering = 2
            ret.VisualTreatment = 1
            ret.VSyncOn = True
            ret.WorldLodLevel = 3
            return ret

        @classmethod
        def min(cls):
            ret = cls()
            ret.CarEnvironmentMapEnable = 0
            ret.CarEnvironmentMapUpdateData = 0
            ret.CarLodLevel = 0
            ret.FSAALevel = 0
            ret.MotionBlurEnable = False
            ret.OverBrightEnable = False
            ret.ParticleSystemEnable = False
            ret.PerformanceLevel = 0
            ret.RainEnable = False
            ret.RoadReflectionEnable = False
            ret.ShadowDetail = 0
            ret.TextureFiltering = 0
            ret.VisualTreatment = 0
            ret.VSyncOn = True
            ret.WorldLodLevel = 0
            return ret

        def __init__(self):
            self.CarEnvironmentMapEnable = None             # int: 0 - 3
            self.CarEnvironmentMapUpdateData = None         # int: 0 - 1
            self.CarLodLevel = None                         # int: 0 - 1
            self.FSAALevel = None                           # int: 0 - 3
            self.MotionBlurEnable = None                    # bool
            self.OverBrightEnable = None                    # bool
            self.ParticleSystemEnable = None                # bool
            self.PerformanceLevel = None                    # int: 0 - 5
            self.RainEnable = None                          # bool
            self.RoadReflectionEnable = None                # bool
            self.ShadowDetail = None                        # int: 0 - 2
            self.TextureFiltering = None                    # int: 0 - 2
            self.VisualTreatment = None                     # int: 0 - 1
            self.VSyncOn = None                             # bool
            self.WorldLodLevel = None                       # int: 0 - 3

        def check(self, raise_exception=False):
            lst = self.min()
            hst = self.max()
            try:
                if not (lst.CarEnvironmentMapEnable <= self.CarEnvironmentMapEnable <= hst.CarEnvironmentMapEnable):
                    raise ValueError("CarEnvironmentMapEnable")
                if not (lst.CarEnvironmentMapUpdateData <= self.CarEnvironmentMapUpdateData <= hst.CarEnvironmentMapUpdateData):
                    raise ValueError("CarEnvironmentMapUpdateData")
                if not (lst.CarLodLevel <= self.CarLodLevel <= hst.CarLodLevel):
                    raise ValueError("CarLodLevel")
                if not (lst.FSAALevel <= self.FSAALevel <= hst.FSAALevel):
                    raise ValueError("FSAALevel")
                if not isinstance(self.MotionBlurEnable, bool):
                    raise ValueError("MotionBlurEnable")
                if not isinstance(self.OverBrightEnable, bool):
                    raise ValueError("OverBrightEnable")
                if not isinstance(self.ParticleSystemEnable, bool):
                    raise ValueError("ParticleSystemEnable")
                if not (lst.PerformanceLevel <= self.PerformanceLevel <= hst.PerformanceLevel):
                    raise ValueError("PerformanceLevel")
                if not isinstance(self.RainEnable, bool):
                    raise ValueError("RainEnable")
                if not isinstance(self.RoadReflectionEnable, bool):
                    raise ValueError("RoadReflectionEnable")
                if not (lst.ShadowDetail <= self.ShadowDetail <= hst.ShadowDetail):
                    raise ValueError("ShadowDetail")
                if not (lst.TextureFiltering <= self.TextureFiltering <= hst.TextureFiltering):
                    raise ValueError("TextureFiltering")
                if not (lst.VisualTreatment <= self.VisualTreatment <= hst.VisualTreatment):
                    raise ValueError("VisualTreatment")
                if not isinstance(self.VSyncOn, bool):
                    raise ValueError("VSyncOn")
                if not (lst.WorldLodLevel <= self.WorldLodLevel <= hst.WorldLodLevel):
                    raise ValueError("WorldLodLevel")
                return True
            except ValueError:
                if raise_exception:
                    raise
                else:
                    return False

        def __eq__(self, other):
            return self.__class__ == other.__class__ and self.__dict__.values() == other.__dict__.values()

        def __ne__(self, other):
            return not self.__eq__(other)

    def __init__(self, data_dir):
        GameBase.__init__(self, data_dir)
        GameSupportIconDir.__init__(self, os.path.join(self._dataDir, "icons"))

        self._addStrait(GameStraitNeedConfigDir())

        # directories and files
        self._gameDir = os.path.join(self._dataDir, "Need For Speed Most Wanted")
        self._gameLangDir = os.path.join(self._dataDir, "language-pack")

        # spec
        self._spec = GameSpec()
        if True:
            self._spec.windows_archs = [self.WindowsArch.WIN32]
            self._spec.windows_versions = [self.WindowsVersion.WINXP, "*"]

            self._spec.default_language = "en_US"

            self._spec.languages = []
            if True:
                for fn in os.listdir(self._gameLangDir):
                    if os.path.isdir(os.path.join(self._gameLangDir, fn)):
                        self._spec.languages.append(fn)
                if self._spec.default_language not in self._spec.languages:
                    raise DataDirError("language '%s' is not supported" % (self._spec.default_language))

            self._spec.resolutions = ["640x480", "800x600", "1024x768", "1280x960", "1600x1200"]
            self._spec.can_fullscreen = True
            self._spec.can_windowed = False

        self._curLang = self._spec.default_language

    @property
    def spec(self):
        return self._spec

    @property
    def run_items(self):
        return ["main"]

    def get_title(self, lang):
        return "Need For Speed Underground 2"

    def _prepare(self, context):
        self._context = context
        try:
            # create game files, including language pack files
            self._targetGameDirObj = CopiedAndSymlinkedDir(os.path.join(self._context.drive_c_dirpath, os.path.basename(self._gameDir)),
                                                           [self._gameDir, os.path.join(self._gameLangDir, self._curLang)])
            self._targetGameDirObj.symlink("", recursive=True)

            # copy config files
            self._archiveDir = GameArchiveDir(self._context.drive_c_dirpath)
            self._archiveDir.import_archives(self._context.config_dirpath)

            # create registry keys, including language related keys
            self._reg = GameRegistry(self._context.registry)
            self._reg.importLangRegFile(os.path.join(self._gameLangDir, self._curLang + ".reg"))

            self._openSaveFiles = []
        except BaseException:
            self._cleanup(on_error=True)
            raise

    @MatchContext.edit_or_run
    def get_language(self):
        assert False

    @MatchContext.edit_or_run
    def set_language(self, language):
        assert language == self._spec.default_language

    @MatchContext.edit
    def list_profiles(self):
        assert False

    @MatchContext.edit
    def open_profile(self, filename):
        retObj = GameProfile(self, filename, os.path.join(self._targetGameDirObj.path, filename))
        self._openSaveFiles.append(filename)                                        # allows opened one file multiple times simutanously
        return retObj

    @MatchContext.edit
    def delete_profile(self, filename):
        assert filename not in self._openSaveFiles
        os.unlink(os.path.join(self._targetGameDirObj.path, filename))

    @MatchContext.run
    def apply_resolution(self, resolution):
        self._reg.writeResolution(resolution)

    @MatchContext.run
    def apply_fullscreen_or_windowed(self, fullscreen_or_windowed):
        assert fullscreen_or_windowed

    @MatchContext.run
    def apply_level_of_detail(self, value):
        assert isinstance(value, self.LevelOfDetail) and value.check(raise_exception=True)
        self._reg.writeLevelOfDetail(value)

    def _commit(self):
        pass

    def _getRunInfo(self, item):
        assert not self._context.edit_or_run
        assert item == Util.listGetOnlyItem(self.run_items)

        # game will crash in wine if cwd is not this directory
        # it must be a wine restriction, but we think it's good habit to specify cwd here
        return self.RunInfo("C:\\Need For Speed Most Wanted\\nfsMW.exe", cwd="C:\\Need For Speed Most Wanted")

    def _cleanup(self, on_error=False):
        if hasattr(self, "_openSaveFiles"):
            assert len(self._openSaveFiles) == 0
            del self._openSaveFiles
        if hasattr(self, "_reg"):
            del self._reg
        if hasattr(self, "_archiveDir"):
            if not on_error:
                self._archiveDir.export_archives(self._context.config_dirpath)
            del self._archiveDir
        if hasattr(self, "_targetGameDirObj"):
            del self._targetGameDirObj
        if hasattr(self, "_context"):
            del self._context


class GameRegistry:

    _keyName = "HKEY_LOCAL_MACHINE\\Software\\EA GAMES\\Need For Speed Most Wanted"

    _resolutionToNumberDict = {
        "640x480": 1,
        "800x600": 2,
        "1024x768": 3,
        "1280x960": 4,
        "1600x1200": 5,
    }

    def __init__(self, regObj):
        self._regObj = regObj

    def importLangRegFile(self, langRegFile):
        self._regObj.import_file(langRegFile)

    def writeResolution(self, resolution):
        resolution = self._resolutionToNumberDict[resolution]
        self._regObj.add_value(self._keyName, "g_RacingResolution", "REG_DWORD", resolution)

    def readLevelOfDetail(self):
        ret = NeedForSpeed10.LevelOfDetail()

        lst = NeedForSpeed10.LevelOfDetail.min()
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.CarEnvironmentMapEnable = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.CarEnvironmentMapUpdateData = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.CarLodLevel = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.FSAALevel = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.MotionBlurEnable = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.OverBrightEnable = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.ParticleSystemEnable = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.PerformanceLevel = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.RainEnable = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.RoadReflectionEnable = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.ShadowDetail = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.TextureFiltering = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.VisualTreatment = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.VSyncOn = bool(self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur)))
        if True:
            cur = "CarEnvironmentMapEnable"
            ret.WorldLodLevel = self._regObj.get_value(self._keyName, "g_%s" % (cur), "REG_DWORD", fallback=getattr(lst, cur))

        return ret

    def writeLevelOfDetail(self, lod):
        assert lod.check()
        for k, v in lod.__dict__.items():
            self._regObj.add_value(self._keyName, "g_%s" % (k), "REG_DWORD", int(v))


class GameArchiveDir:

    def __init__(self, drive_c_dirpath):
        self._username = pwd.getpwuid(os.geteuid()).pw_name
        self._docDir = os.path.join(drive_c_dirpath, "users", self._username, "Documents")

    def import_archives(self, backup_archive_dir):
        srcDir = os.path.join(backup_archive_dir, "NFS Most Wanted")
        dstDir = os.path.join(self._docDir, "NFS Most Wanted")
        if os.path.isdir(srcDir):
            Util.forceCopy(srcDir, dstDir)
        else:
            Util.forceDelete(dstDir)

    def export_archives(self, backup_archive_dir):
        srcDir = os.path.join(self._docDir, "NFS Most Wanted")
        dstDir = os.path.join(backup_archive_dir, "NFS Most Wanted")
        if os.path.isdir(srcDir):
            Util.forceCopy(srcDir, dstDir)
        else:
            Util.forceDelete(dstDir)


class GameProfile:
    pass


class SaveFile:

    # https://www.nfsunlimited.net/forum/viewtopic.php?t=12706

    class Car:

        def __init__(self):
            self.is_sold = False
            self.id = None
            self.bounty = None

    class ParseError(Exception):
        pass

    def __init__(self, parent, filename, path):
        self._parent = parent
        self._filename = filename
        self._path = path

        self._account = None
        self._money = None
        self._carList = []
        with open(self._path, "rb+") as inptr:
            if inptr.read(4) != b'20CM':
                raise self.ParseError("not a Need for Speed: Most Wanted save file")
            if True:
                assert False

        self._bChanged = False

    @property
    def path(self):
        return self._path

    @property
    def changed(self):
        return self._bChanged

    def get_alias(self):
        return self._account

    def get_money(self):
        return self._money

    def set_alias(self, alias):
        self._account = alias
        self._bChanged = True

    def set_money(self, money):
        assert 0 <= money <= 2**31 - 1

        self._money = money
        self._bChanged = True

    def save(self):
        if not self._bChanged:
            return

        with open(self._path, "rb+") as inptr:
            assert False

        self._bChanged = False
